#include <wx/textctrl.h>

#include "render.h"
#include "settings.h"
#include "util.h"

void TextItem::Render(wxTextCtrl* textctrl, const RenderOptions& opt) const
{
    textctrl->SetValue("");
    TextLayout layout;

    switch(_type) {
    case INFO: {
        layout.AppendMarkText(info());
        RenderLayout(textctrl, layout, opt);
        break; }
    case NOTE: {
        RenderNote(textctrl, opt);
        break; }
    case KEYWORD: {
        layout.AppendRegion(keyword().sortname, TextRegion::Heading);
        if (!opt.collapse) {
            if (!keyword().aliases.IsEmpty()) {
                layout.AppendRegion("\nAliases: ", TextRegion::Text);
                layout.AppendRegion(keyword().aliases, TextRegion::Keyword);
            }
            if (!keyword().desc.IsEmpty()) {
                layout.AppendRegion("\n\n", TextRegion::Text);
                layout.AppendMarkText(keyword().desc);
            }
        }
        RenderLayout(textctrl, layout, opt);
        break; }
    }
}

void TextItem::RenderNote(wxTextCtrl* textctrl, const RenderOptions& opt) const
{
    wxASSERT(isNote());
    TextLayout layout;

    // collapsed?
    if (opt.collapse) {

        // caption
        if (!_note.caption.IsEmpty())
            layout.AppendRegion(_note.caption, TextRegion::Heading);
        else
            layout.AppendRegion(wxFormat(_("note #%u"), _note.id), TextRegion::Heading);
        layout.AppendRegion(" (", TextRegion::Text);

        // date
        if (!_note.date.IsEmpty())
            layout.AppendRegion(_note.date, TextRegion::Date);
        else
            layout.AppendRegion(_("<date not specified>"), TextRegion::Date);
        layout.AppendRegion(")", TextRegion::Text);

    // not collapsed
    } else {
        bool has_header = false;

        // write caption
        if (!_note.caption.IsEmpty()) {
            layout.AppendRegion(_note.caption, TextRegion::Heading);
            layout.AppendRegion("\n", TextRegion::Text);
            has_header = true;
        }

        // write date
        if (!_note.date.IsEmpty()) {
            layout.AppendRegion(_note.date, TextRegion::Date);
            layout.AppendRegion("\n", TextRegion::Text);
            has_header = true;
        }

        // extra line between header and text
        if (has_header)
            layout.AppendRegion("\n", TextRegion::Text);

        layout.AppendMarkText(_note.text);
    }

    // render
    RenderLayout(textctrl, layout, opt);
}


/// Removes name of source from 'text' (which is expected to be a source citation),
/// if it is citing the same source given in 'last_source'.
/// Correctly handles indirect source citation, or omitted name of direct source.
static void StripSource(wxString& text, wxString& last_source)
{
    if (text.IsEmpty())
        return;

    // find beginning and end of direct source
    wxASSERT(text[0] == '(');
    wxASSERT(text.Last() == ')');
    int from = 1, to = text.Len()-1;

    // skip indirect source
    wxString direct_source = text.Mid(from, to-1);
    static const wxChar *indirect_prefix = L"; podla ";
    #define indirect_prefix_len 8
    int indirect_prefix_pos = text.Find(indirect_prefix);
    if (indirect_prefix_pos != wxNOT_FOUND) {
        from = indirect_prefix_pos + indirect_prefix_len;
        direct_source = direct_source.Mid(from-1, to-1);
    }

    // page number
    bool has_page_number = false;
    static const wxChar *page_number_prefix = L", s. ";
    #define page_number_prefix_len 5
    int page_number_pos = direct_source.Find(page_number_prefix);
    if (page_number_pos != wxNOT_FOUND) {
        has_page_number = true;
        to = from + page_number_pos;
        direct_source = direct_source.Mid(0, page_number_pos);
    }

    // no direct source?
    if (direct_source.StartsWith("s. ") || direct_source.IsEmpty())
        return;

    // current direct source is different as previous one?
    if (direct_source != last_source) {
        last_source = direct_source;
        return;
    }

    // same => replace source in text to be displayed
    if (has_page_number) {
        text.Remove(from, to-from + page_number_prefix_len);
        text.insert(from, "s. ");
    } else {
        text.Remove(from, to-from);
        text.insert(from, "ibid");
    }
}

/// Sets default style of wxTextCtrl.
/// This is kind-of workaround for lack of direct method in wxTextCtrl to reset only specific attributes
/// of default style to default values.
static void SetStyle(wxTextCtrl* textctrl, wxTextAttr style)
{
    // This is the only way to reset wxTextCtrl default style atttribute back to default.
    textctrl->SetDefaultStyle(wxTextAttr());
    textctrl->SetDefaultStyle(style);
}

/// Renders string 'text' to 'textctrl', using 'text_style', except for all occurences of
/// 'hightlight_keyword', which are rendered using 'highlight_style'.
/// (case insensitive)
static void RenderTextWithHighlight(
            wxTextCtrl*         textctrl,
            const wxString&     text,
            const wxString&     highlight_keyword,
            const wxTextAttr&   text_style,
            const wxTextAttr&   highlight_style
            )
{
    int word, last_word_end=0, highlight_len = highlight_keyword.Len();
    while (wxNOT_FOUND != (word = wxString_FindI(text, highlight_keyword, last_word_end))) {
        SetStyle(textctrl, text_style);
        textctrl->AppendText(text.Mid(last_word_end, word - last_word_end));
        SetStyle(textctrl, highlight_style);
        textctrl->AppendText(text.Mid(word, highlight_len));
        last_word_end = word + highlight_len;
    }
    if (last_word_end < (int)text.Len()) {
        SetStyle(textctrl, text_style);
        textctrl->AppendText(text.Mid(last_word_end));
    }
}

// TODO: maybe this (and helpers) shouldn't be member of TextItem?
/// Renders (writes text and sets styles) the TextLayout 'layout' to wxTextCtrl 'textctrl',
/// according to RenderOptions 'opt' and global font configurations in 'config'.
void TextItem::RenderLayout(wxTextCtrl* textctrl, TextLayout layout, const RenderOptions& opt) const
{
    textctrl->Freeze();

    // set up text styles (combined with default style)
    #define defattr(x) wxTextAttr::Merge(config->fonts[FONT_DEFAULT].attr, x);
    const wxTextAttr& text_style            = config->fonts[FONT_DEFAULT].attr;
    const wxTextAttr& caption_style         = defattr(config->fonts[FONT_CAPTION].attr);
    const wxTextAttr& date_style            = defattr(config->fonts[FONT_DATE].attr);
    const wxTextAttr& keyword_style         = defattr(config->fonts[FONT_KEYWORD].attr);
    const wxTextAttr& keyword_mark_style    = defattr(config->fonts[FONT_KEYWORD_MARK].attr);
    const wxTextAttr& source_style          = defattr(config->fonts[FONT_SOURCE].attr);
    const wxTextAttr& error_style           = defattr(config->fonts[FONT_ERROR].attr);
    const wxTextAttr& unknown_keyword_style = defattr(config->fonts[FONT_UNK_KEYWORD].attr);
    #undef defattr

    // TODO: for now style 0 is used for text, and following are used for keywords
    #define MAX_HIGHTLIGHT_STYLES 6
    #define HIGHLIGHT_TEXT 0
    wxTextAttr highlight_style[MAX_HIGHTLIGHT_STYLES];
    highlight_style[0] = wxTextAttr(wxNullColour, wxColour(0x80, 0xFF, 0x80));  // light green
    highlight_style[1] = wxTextAttr(wxNullColour, wxColour(0xB0, 0xB0, 0xFF));  // light blue
    highlight_style[2] = wxTextAttr(wxNullColour, wxColour(0xFF, 0xFF, 0x80));
    highlight_style[3] = wxTextAttr(wxNullColour, wxColour(0x80, 0xFF, 0xFF));
    highlight_style[4] = wxTextAttr(wxNullColour, wxColour(0xFF, 0x80, 0xFF));
    highlight_style[5] = wxTextAttr(wxNullColour, wxColour(0xA0, 0x52, 0x2D)); //Sienna brown
    for (int x=0; x<MAX_HIGHTLIGHT_STYLES; x++)
        highlight_style[x].SetFlags(wxTEXT_ATTR_BACKGROUND_COLOUR);

    // display all regions in the layout description
    wxString last_source = "";
    for (int region=0; region<layout.RegionCount(); region++) {
        wxString region_text = layout.RegionText(region);
        TextRegion region_desc = layout.RegionDesc(region);

        switch(region_desc.type) {

        case TextRegion::Heading:

            // highlight keywords?
            if (opt.highlight && !opt.highlight_body.IsEmpty()) {
                RenderTextWithHighlight(textctrl, region_text, opt.highlight_body, caption_style, highlight_style[0]);
                continue;
            }

            SetStyle(textctrl, caption_style);
            break;

        case TextRegion::Date:
            SetStyle(textctrl, date_style);
            break;

        case TextRegion::Text:

            // highlight keywords?
            if (opt.highlight && !opt.highlight_body.IsEmpty()) {
                RenderTextWithHighlight(textctrl, region_text, opt.highlight_body, text_style, highlight_style[0]);
                continue;
            }

            SetStyle(textctrl, text_style);
            break;

        case TextRegion::Source:

            // show just upper-index 'x' if sources display is disabled
            if (!opt.show_sources)
                region_text = "\u02e3"; // latin modifier small letter X
            else {
                SetStyle(textctrl, source_style);

                // highlight entire source, if it contains searched string
                if (opt.highlight && !opt.highlight_body.IsEmpty()
                && (wxNOT_FOUND != wxString_FindI(region_text, opt.highlight_body))) {
                    SetStyle(textctrl, highlight_style[0]);
                }

                // remove source name if it's same as last time
                StripSource(region_text, last_source);
            }

            break;

        case TextRegion::KeywordMark:
            if (!opt.show_keyword_marks)
                continue;
            SetStyle(textctrl, keyword_mark_style);
            break;

        case TextRegion::Keyword: {

            // highlight entire keyword, if it matches any of searched keywords
            wxTextAttr highlight_attr;
            if (opt.highlight) {
                for (unsigned i=0; i<opt.highlight_keywords.size(); i++) {
                    const wxString& keyword = opt.highlight_keywords[i];
                    if (wxNOT_FOUND != wxString_FindI(region_desc.data, keyword)) {
                        highlight_attr = highlight_style[(1+i) % MAX_HIGHTLIGHT_STYLES]; // first style is for text highlight
                        break;
                    }
                }
            }

            // unknown keyword?
            // keyword '--known-keyword--' is always known, an exception made for config dialog
            if (opt.show_unknown_keywords && region_desc.data != "--known-keyword--") {
                if (!region_desc.data.IsEmpty() && !keyword_list.ContainsName(region_desc.data)) {
                    SetStyle(textctrl, wxTextAttr::Merge(unknown_keyword_style, highlight_attr));
                    break;
                }
            }

            SetStyle(textctrl, wxTextAttr::Merge(keyword_style, highlight_attr));
            break;
        }

        case TextRegion::Error:
            SetStyle(textctrl, error_style);
            break;
        default:
            wxASSERT(1);
        }
        textctrl->AppendText(region_text);
    }
    textctrl->Thaw();
}

wxString TextItem::ToString() const
{
    switch(type()) {
        case INFO:    return wxFormat("Info: '%s'", _info);
        case KEYWORD: return wxFormat("Keyword #%u", _keyword.id);
        case NOTE:    return wxFormat("Note #%u", _note.id);
    }
    return "";
}
